package com.refusea.train.domain.helper

import com.alibaba.fastjson2.JSON
import com.refusea.train.config.mvc.WebSession
import com.refusea.train.domain.cache.ClassGroupCache
import com.refusea.train.domain.cache.ClassGroupStudentCache
import com.refusea.train.domain.cache.ClassroomCache
import com.refusea.train.domain.cache.CourseCache
import com.refusea.train.domain.cache.UserCache
import com.refusea.train.domain.entity.ClassGroup
import com.refusea.train.domain.entity.LessonConfirmed
import com.refusea.train.domain.entity.LessonSchedule
import com.refusea.train.domain.entity.User.Role.PARENT
import com.refusea.train.domain.entity.User.Role.PRINCIPAL
import com.refusea.train.domain.entity.User.Role.STUDENT
import com.refusea.train.domain.entity.User.Role.TEACHER
import com.refusea.train.domain.ex.BizEx
import com.refusea.train.domain.req.LessonHistoryReq
import com.refusea.train.domain.req.ManualScheduleReq
import com.refusea.train.domain.req.ScheduleQueryReq
import com.refusea.train.domain.tx.LessonScheduleTx
import com.refusea.train.domain.vo.IdName
import com.refusea.train.domain.vo.LessonScheduleStatus
import com.refusea.train.domain.vo.LessonScheduleSummary
import com.refusea.train.domain.vo.OccupiedSchedule
import com.refusea.train.domain.vo.PaginationVo
import com.refusea.train.domain.vo.RecentLessonSchedule
import com.refusea.train.mapper.ClassGroupMapper
import com.refusea.train.mapper.ClassGroupStudentMapper
import com.refusea.train.mapper.CourseMapper
import com.refusea.train.mapper.LessonConfirmedMapper
import com.refusea.train.mapper.LessonScheduleConfigMapper
import com.refusea.train.mapper.LessonScheduleMapper
import com.refusea.train.mapper.UserMapper
import org.springframework.stereotype.Component
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAdjusters

/**
 * @author Zhang Yan 2024/4/12
 **/
@Component
class LessonScheduleHelper(
    private val lessonScheduleMapper: LessonScheduleMapper,
    private val lessonScheduleConfigMapper: LessonScheduleConfigMapper,
    private val classGroupMapper: ClassGroupMapper,
    private val classGroupStudentMapper: ClassGroupStudentMapper,
    private val courseMapper: CourseMapper,
    private val lessonConfirmedMapper: LessonConfirmedMapper,
    private val userMapper: UserMapper,
    private val userCache: UserCache,
    private val classroomCache: ClassroomCache,
    private val courseCache: CourseCache,
    private val classGroupCache: ClassGroupCache,
    private val classGroupStudentCache: ClassGroupStudentCache,
    private val lessonScheduleTx: LessonScheduleTx,
) {

    private val limit = 10
    private val tf = DateTimeFormatter.ofPattern("HH:mm")

    fun summary(): List<ClassGroup> {

        val schoolId = WebSession.user().schoolId
        val classGroups = classGroupMapper.active(schoolId)
        if (classGroups.isEmpty()) {
            return emptyList()
        }

        val today = LocalDate.now()

        val thisMonday = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
        val thisSunday = thisMonday.plusDays(6)

        var summaries = lessonScheduleMapper.summaries(schoolId, thisMonday, thisSunday.plusDays(1))
        var map = summaries.associateBy { it.classGroupId }
        classGroups.forEach {
            val summary = map[it.id] ?: LessonScheduleSummary(it.id, null)
            summary.start = thisMonday
            summary.end = thisSunday
            it.thisWeek = summary
        }

        val nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
        val nextSunday = nextMonday.plusDays(6)
        summaries = lessonScheduleMapper.summaries(schoolId, nextMonday, nextSunday.plusDays(1))
        map = summaries.associateBy { it.classGroupId }
        classGroups.forEach {
            val summary = map[it.id] ?: LessonScheduleSummary(it.id, null)
            summary.start = nextMonday
            summary.end = nextSunday
            it.nextWeek = summary
        }

        classGroups.forEach {
            it.classroom = classroomCache.get(it.classroomId)
            it.course = courseCache.get(it.courseId)
            it.teacher = userCache.get(it.course!!.teacherId)
            it.students = classGroupStudentCache.students(it.id)
                .mapNotNull { id -> userCache.get(id) }
        }

        return classGroups
    }

    fun list(classGroupId: Int, thisWeek: Boolean): List<LessonSchedule> {

        val today = LocalDate.now()
        val monday = if (thisWeek) {
            today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
        } else {
            today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
        }
        val nextMonday = monday.plusDays(7)


        return lessonScheduleMapper.list(WebSession.user().schoolId, classGroupId, monday, nextMonday)
    }

    fun occupied(classGroupId: Int, thisWeek: Boolean, half: Boolean? = null): OccupiedSchedule {
        val today = LocalDate.now()
        val monday = if (thisWeek) {
            today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
        } else {
            today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
        }
        val nextMonday = monday.plusDays(7)

        val classGroup = classGroupMapper.select(classGroupId) ?: throw BizEx.of("班级不存在")
        val course = courseMapper.select(classGroup.courseId) ?: throw BizEx.of("课程不存在")

        val schoolId = WebSession.user().schoolId
        val h = half ?: lessonScheduleConfigMapper.select(schoolId)?.halfHour ?: throw BizEx.of("排课配置不存在")

        val date = lessonScheduleMapper.occupied(
            schoolId,
            classGroupId,
            classGroup.classroomId,
            course.teacherId,
            monday,
            nextMonday
        )

        return OccupiedSchedule(
            if (h) {
                date.flatMap { listOf(it, it.plusMinutes(30)) }
            } else {
                date
            },
            lessonScheduleMapper.maxLessonNumber(schoolId, classGroupId) ?: 0
        )
    }

    fun save(req: ManualScheduleReq) {

        val schoolId = WebSession.user().schoolId

        val classGroup = classGroupMapper.select(req.classGroupId) ?: throw BizEx.of("班级不存在")
        if (classGroup.schoolId != schoolId) {
            throw BizEx.SCHOOL_ERROR
        }

        val config = lessonScheduleConfigMapper.select(schoolId) ?: throw BizEx.of("排课配置不存在")
        val half = config.halfHour

        val course = courseMapper.select(classGroup.courseId) ?: throw BizEx.of("课程不存在")

        val occupied = occupied(req.classGroupId, req.thisWeek, half)

        val maxLessonNumber = occupied.maxLessonNumber

        val rest = (course.lessonCount - maxLessonNumber) * if (half) {
            2
        } else {
            1
        }
        val count = req.values.sumOf { it?.size ?: 0 }
        if (count > rest) {
            throw BizEx.of("排课数量（${count}）超过剩余课时数量（${rest}）")
        }

        if (half) {
            if (req.values.filterNotNull().any {
                    it.size % 2 != 0 ||
                            // check if schedule continuous: first time is before 30 minutes of second time
                            it.windowed(2, 2).any { e ->
                                val t0 = LocalTime.parse(e[0], tf)
                                val t1 = LocalTime.parse(e[1], tf)
                                val diff = if (t0 < t1) {
                                    t0.until(t1, ChronoUnit.MINUTES)
                                } else {
                                    t1.until(t0, ChronoUnit.MINUTES)
                                }
                                diff != 30L
                            }
                }) {
                throw BizEx.of("排课时间段不连续")
            }
        }

        val today = LocalDate.now()
        val monday = if (req.thisWeek) {
            today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
        } else {
            today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
        }

        val occupiedDate = occupied.date.toSet()

        req.values.withIndex().forEach { (index, value) ->
            if (value != null &&
                value.any { e ->
                    occupiedDate.contains(
                        LocalDateTime.of(monday.plusDays(index.toLong()), LocalTime.parse(e, tf))
                    )
                }
            ) {
                throw BizEx.of("排课时间已被占用：星期${index + 1}, $value")
            }
        }

        var lessonNumber = maxLessonNumber
        val list = req.values.withIndex().flatMap { (index, value) ->
            if (value.isNullOrEmpty()) {
                emptyList()
            } else {
                value.sorted().windowed(2, 2)
                    .map { e ->
                        val time = LocalDateTime.of(monday.plusDays(index.toLong()), LocalTime.parse(e[0], tf))
                        lessonNumber += 1
                        LessonSchedule(
                            id = 0,
                            schoolId = schoolId,
                            classGroupId = req.classGroupId,
                            lessonNumber = lessonNumber,
                            lessonTime = time,
                            published = !req.draft,
                            false
                        )
                    }
            }
        }

        lessonScheduleTx.save(
            list,
            if (list[0].published) {
                list.groupBy { it.classGroupId }.mapValues { (_, group) -> group.size }
            } else {
                emptyMap()
            }
        )
    }

    fun unconfirmedCount(): Int {
        val user = WebSession.user()
        return lessonScheduleMapper.unconfirmedCount(user.schoolId, user.id)
    }

    fun unconfirmed(): List<LessonSchedule> {
        val user = WebSession.user()
        val list = lessonScheduleMapper.unconfirmed(user.schoolId, user.id)
        list.forEach {
            it.classGroup = classGroupCache.get(it.classGroupId)
            it.course = courseCache.get(it.classGroup!!.courseId)
            it.classroom = classroomCache.get(it.classGroup!!.classroomId)
            it.students = classGroupStudentCache.students(it.classGroupId)
                .mapNotNull { id -> userCache.get(id) }
        }
        return list
    }

    fun confirm(ids: List<Int>?) {
        if (ids.isNullOrEmpty()) throw BizEx.of("参数错误")

        val uid = WebSession.user().id

        var list = lessonScheduleMapper.listByIds(ids)
        if (list.any { it.schoolId != WebSession.user().schoolId }) {
            throw BizEx.SCHOOL_ERROR
        }

        list = list.filter { !it.confirmed }.filter { it.published }
        list.forEach {
            it.classGroup = classGroupCache.get(it.classGroupId)
            it.course = courseCache.get(it.classGroup!!.courseId)
        }
        list = list.filter { it.course!!.teacherId == uid }
        list.forEach {
            it.classroom = classroomCache.get(it.classGroup!!.classroomId)
            it.students = classGroupStudentCache.students(it.classGroupId)
                .mapNotNull { id -> userCache.get(id) }
        }

        val confirmedList = list.map {
            val confirmed = LessonConfirmed(
                0,
                it.schoolId,
                it.course!!.id,
                uid,
                it.classGroup!!.name,
                it.classroom!!.name,
                it.lessonNumber,
                it.lessonTime,
                it.course!!.salary * it.students!!.size,
                JSON.toJSONString(it.students!!.map { e -> IdName(e.id, e.name) })
            )
            confirmed.scheduleId = it.id
            confirmed.classGroupId = it.classGroupId
            confirmed.course = it.course

            if (it.lessonNumber == it.course!!.lessonCount) {
                confirmed.studentIds = it.students!!.map { e -> e.id }
            }
            confirmed
        }



        lessonScheduleTx.confirm(
            confirmedList,
            confirmedList.groupBy { it.classGroupId!! }.mapValues { (_, group) -> group.size })
    }

    fun history(req: LessonHistoryReq): PaginationVo {

        val user = WebSession.user()
        val schoolId = user.schoolId
        req.schoolId = schoolId
        req.calculateOffset(limit)
        req.end = req.end?.plusDays(1)
        if (user.role == TEACHER) {
            req.teacherId = user.id
        }

        val total = lessonConfirmedMapper.count(req)
        if (total == 0) {
            return PaginationVo.empty(req.page, limit)
        }

        return PaginationVo(
            req.page,
            limit,
            total,
            if (req.offset >= total) {
                emptyList()
            } else {
                val list = lessonConfirmedMapper.query(req)
                list.forEach {
                    it.course = courseCache.get(it.courseId)
                    it.teacher = userCache.get(it.teacherId)
                }
                list
            }
        )
    }

    fun schedules(req: ScheduleQueryReq): List<LessonSchedule> {

        val user = WebSession.user()
        req.schoolId = user.schoolId
        req.monday = LocalDate.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))

        when (user.role) {
            TEACHER -> req.teacherId = user.id
            PARENT -> req.parentId = user.id
            STUDENT -> req.studentId = user.id
            else -> {}
        }

        val students =
            if (req.studentId != 0) {
                setOf(req.studentId)
            } else if (req.parentId != 0) {
                userMapper.childrenIds(req.parentId).toSet().plus(req.parentId)
            } else {
                emptySet()
            }

        if (students.isNotEmpty()) {
            req.classGroupIds = classGroupStudentMapper.classGroupIds(user.schoolId, students)
        }


        val list = lessonScheduleMapper.schedules(req)
        list.forEach {
            it.classGroup = classGroupCache.get(it.classGroupId)
            it.course = courseCache.get(it.classGroup!!.courseId)
            it.classroom = classroomCache.get(it.classGroup!!.classroomId)
            it.teacher = userCache.get(it.course!!.teacherId)
            it.students = classGroupStudentCache.students(it.classGroupId)
                .mapNotNull { id -> userCache.get(id) }
        }

        return if (students.isEmpty()) {
            list
        } else {
            list.filter { it.students!!.any { student -> students.contains(student.id) } }
        }.sortedWith(compareBy<LessonSchedule> { it.classGroupId }.thenByDescending { it.lessonNumber })

    }

    fun recent(): List<RecentLessonSchedule> {
        val user = WebSession.user()

        val start = LocalDateTime.now().minusMinutes(15)
        val end = LocalDate.now().plusDays(2)
        val schedules = lessonScheduleMapper.recent(user.schoolId, start, end)

        if (schedules.isEmpty()) {
            return emptyList()
        }

        schedules.forEach {
            it.classGroup = classGroupCache.get(it.classGroupId)
            it.course = courseCache.get(it.classGroup!!.courseId)
            it.classroom = classroomCache.get(it.classGroup!!.classroomId)
        }

        return when (user.role) {
            TEACHER, PRINCIPAL, STUDENT -> schedules
                .filter {
                    if (user.role == STUDENT) {
                        classGroupStudentCache.students(it.classGroupId).contains(user.id)
                    } else {
                        it.course!!.teacherId == user.id
                    }
                }
                .map {
                    RecentLessonSchedule(
                        it.id,
                        it.course!!.name,
                        it.classroom!!.name,
                        it.lessonTime,
                        user.id,
                        user.name
                    )
                }.sortedBy { it.time }

            PARENT -> {
                val ids = listOf(user.id) + userMapper.childrenIds(user.id)
                schedules.flatMap {
                    val students = classGroupStudentCache.students(it.classGroupId).toSet()
                    ids.filter { id -> students.contains(id) }.map { id ->
                        RecentLessonSchedule(
                            it.id,
                            it.course!!.name,
                            it.classroom!!.name,
                            it.lessonTime,
                            id,
                            userCache.get(id)!!.name
                        )
                    }
                }.sortedWith(compareBy<RecentLessonSchedule> { it.uid }.thenBy { it.time })
            }

            else -> emptyList()
        }
    }

    fun scheduleStatus(): List<LessonScheduleStatus> {

        val schoolId = WebSession.user().schoolId
        val classGroups = classGroupMapper.active(schoolId)
        if (classGroups.isEmpty()) {
            return emptyList()
        }

        val today = LocalDate.now()

        var monday = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
        var summaries = lessonScheduleMapper.summaries(schoolId, monday, monday.plusDays(7))
        var map = summaries.associateBy { it.classGroupId }
        var result = classGroups.map {
            LessonScheduleStatus(true, it.id, it.name, map[it.id]?.published)
        }

        if (today.dayOfWeek.value > 4) {
            monday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
            summaries = lessonScheduleMapper.summaries(schoolId, monday, monday.plusDays(7))
            map = summaries.associateBy { it.classGroupId }
            result = result + classGroups.map {
                LessonScheduleStatus(false, it.id, it.name, map[it.id]?.published)
            }
        }

        return result
    }
}
